返回笔记

数字电路 - 电路代码与RTL语言

目录
目录

编码、译码和选择

编码,译码和选择这三个电路都是组合逻辑电路里的标准模块,就像乐高里的标准零件一样,用处非常大。

编码 (器) - 把分散变集中

1. 核心思想是什么?

  • 问题:想象一下你的键盘有104个按键,如果每个按键都单独连一根线到电脑主板,那得多少根线?太浪费了!
  • 编码器的作用:就是用少量的几根线来表示大量的输入状态。它把每一个输入信号(比如你按下的某个键)转换成一个唯一的二进制代码。
  • 一句话总结多输入 -> 少输出,给每个输入一个“代号”。

2. 普通编码器

  • 例子:数字 0-9 对应7段数码管的显示。
    • 输入: D₀, D₁, ..., D₉ (10个输入,代表你想显示哪个数字)。
    • 输出: a, b, c, d, e, f, g (7个输出,控制数码管的哪几段亮)。
    • 比如,当输入 D₁ 有效时(你想显示数字1),输出端 bc 就要有效(让这两段亮起来)。
    • 我们可以为每一段写出逻辑表达式,比如 a 段在显示 0, 2, 3, 5, 6, 7, 8, 9 时都会亮,所以它的逻辑就是:
      a = D₀ + D₂ + D₃ + D₅ + D₆ + D₇ + D₈ + D₉

3. 优先编码器 (Priority Encoder) - 解决“冲突”

  • 问题:如果普通编码器有多个输入同时有效(比如你不小心同时按下了 AS 键),输出的二进制码就会混乱,不知道该听谁的。

  • 优先编码器的作用:它为所有输入信号设定了一个优先级。当有多个输入同时有效时,它只理会优先级最高的那个,并输出它的二进制代码。

  • 看懂紧凑真值表
    image.png

    • X 代表“无关项 (Don't Care)”,意思是“我不在乎这个输入是0还是1”。
    • 比如表格里 D₃ 的优先级最高。只要 D₃=1,不管 D₂, D₁, D₀ 是什么,输出就是 D₃ 的编码 11 (假设编码是 A₁A₀)。
    • V (Valid) 是一个有效位,通常用来表示“当前有没有任何一个输入是有效的”。

译码 (器) - 把代号变回信号

1. 核心思想是什么?

  • 译码是编码的逆过程
  • 问题:电脑给你一个二进制代码(比如学号 011),你怎么用这个代码准确地找到对应的那个人(比如3号同学)?
  • 译码器的作用:将输入的少量二进制代码,转换成大量的、唯一的输出信号。它把输入的“代号”翻译成具体的指令或位置。
  • 一句话总结少输入 -> 多输出,根据代号激活唯一一个目标。

2. 它是如何工作的?

  • 一个 n 输入的译码器,最多可以有 2ⁿ 个输出。

  • 本质:译码器就是一个最小项生成器。每一个输出引脚都对应一个唯一的输入组合(即一个最小项)。

  • 例子:3-8译码器
    image.png
    image.png
    image.png

    • 有3个输入 (A₂, A₁, A₀) 和8个输出 (D₀ - D₇)。
    • 当输入为 101 (二进制的5) 时,只有输出引脚 D₅ 会被激活(比如变为高电平 1),其他所有输出都保持无效(低电平 0)。

3. 译码器的妙用

  • 用小译码器搭大译码器:就像 PPT 里展示的,可以用两个2-4译码器和一个1-2译码器(或使能端)来搭建一个3-8译码器。这是层次化设计思想的体现。

  • 实现任何组合逻辑函数:这是译码器一个非常强大的功能!

    • 因为译码器能产生所有最小项,所以任何一个可以写成“最小项之和”的函数,都可以用一个译码器 + 一个或门来实现。

    • 例子:实现1位全加器
      image.png

      • S = Σm(1, 2, 4, 7)
      • 进位 C = Σm(3, 5, 6, 7)
      • 我们只需要把译码器的 D₁, D₂, D₄, D₇ 这四个输出接到一个或门,这个或门的输出就是 S。同理,把 D₃, D₅, D₆, D₇ 接到另一个或门,输出就是 C

选择 (器) / 多路复用器 (MUX) - 多选一的开关

1. 核心思想是什么?

  • 问题:你家里有多台设备(电视、电脑、游戏机),但只有一个音响。你希望通过一个开关来决定,当前是哪台设备的声音从音响里播放出来。
  • 选择器的作用:它就像一个数据选择开关。它有很多路“数据输入”,几根“选择输入”,和一路“输出”。“选择输入”的二进制值决定了哪一路“数据输入”会被连接到唯一的“输出”上。
  • 一句话总结根据选择信号,从多个输入中选一个送到输出

2. 它是如何工作的?

  • 一个 2ⁿ 输入的 MUX,需要 n 根选择线。

  • 例子:2-1 MUX
    image.png

    • 有2路数据输入 (I₀, I₁),1根选择线 (S),1路输出 (Y)。
    • 逻辑表达式是: Y = S'·I₀ + S·I₁
    • S=0Y = 1·I₀ + 0·I₁ = I₀,输出接通 I₀。
    • S=1Y = 0·I₀ + 1·I₁ = I₁,输出接通 I₁。
  • 紧凑真值表

    • 这种真值表更直观,直接写出当选择信号 S 为某个值时,输出 Y 等于哪个输入。
SY
0I₀
1I₁

3. 选择器的妙用

  • 实现组合逻辑函数:和译码器一样,选择器也可以用来实现任何组合逻辑函数,而且有时候更灵活、更省成本。
  • 基本思路 (以3变量函数 F (A, B, C) 为例):
    1. 用一个 4-1 MUX。
    2. 把变量 AB 接到 MUX 的选择端 S₁S₀
    3. 根据 A, B 的取值 (00, 01, 10, 11),看函数 F 与剩下的变量 C 是什么关系 (0, 1, C 还是 C')。
    4. 把这个关系(0, 1, CC')连接到对应的 MUX 数据输入端 (I₀, I₁, I₂, I₃)。
    • 这种方法非常巧妙,可以大大减少电路的复杂性。

关系

  • 编码器:多变少,负责“压缩信息”。
  • 译码器:少变多,负责“解释信息”。
  • 选择器 (MUX):多选一,负责“切换信息”。

这三个都是数字系统里不可或缺的基础模块。搞懂了它们各自的使命和工作原理,很多复杂的电路问题都能迎刃而解。


迭代电路与补码加减法器

迭代组合电路

核心思想

  • 问题: 对于位数很宽的操作数电路(如64位加法器),使用传统的真值表方法是不可行的,因为真值表会变得异常庞大(例如,64位加法器输入有129位,真值表高达 2129 行)。
  • 解决方案: 将“宽操作数”电路分解为由许多相同的“窄操作数”单元电路组成的阵列。
  • 迭代方法: 利用每位操作所具有的相似性和单元结构的规整性,通过阵列结构将多个子电路(单元)连接起来,从而简化设计。
  • 迭代阵列 (Iterative Array): 连接单元的架构,可以是一维、二维或三维结构。

一维迭代结构示例

一维迭代结构通常将前一个单元的输出作为后一个单元的输入,实现信息的逐位处理和传递。

二进制加法器

半加器

  • 功能: 将两个二进制位 XY 相加。
  • 输出:
    • 本位和 S (Sum)
    • 进位 C (Carry)
  • 逻辑表达式:
    • S = X ⊕ Y
    • C = X · Y
  • 电路图与真值表:
    image.png

全加器

  • 功能: 将三个二进制位 XY 和低位来的进位 Z (Cin) 相加。
  • 输出:
    • 本位和 S (Sum)
    • 向高位的进位 C (Cout)
  • 逻辑表达式:
    • S = X ⊕ Y ⊕ Z
    • C = XY + (X ⊕ Y)ZC = XY + XZ + YZ
  • 结构: 一个全加器可以由两个半加器和一个或门构成。
    image.png

行波进位加法器

  • 结构: 由多个(如4个)1位全加器迭代构成。
  • 工作方式: 低位的进位信号 C 作为高一位全加器的输入信号,像波浪一样逐位向高位传递。
  • 特点:
    • 优点: 结构简单,成本低。
    • 缺点: 速度很慢,因为高位的计算必须等待所有低位的进位信号都确定后才能进行。
      image.png

补码加减法

1. 机器数与带符号数

  • 真值: 用“+”、“-”表示符号的实际数值。
  • 机器数: 数字在计算机中的二进制表示形式,通常用最高位作为符号位。
    • 符号位: 0表示正数,1表示负数。
  • 编码方式:
    • 正数: 原码、反码、补码表示相同,即“0”+数值的绝对值。
    • 负数: 原码、反码、补码有不同的表示形式。
      • **原码 : 符号位为1,其余位为数值的绝对值。
      • **反码 : 符号位为1,其余位为数值绝对值按位取反。
      • **补码 : 反码的末位加1。

2. 补码加减法运算

  • 核心思想: 将减法运算转换为加法运算。A - B 等价于 A + (-B)。在电路中,这意味着减去一个数等于加上这个数的补数
  • 运算法则:
    1. [A+B]_补 = [A]_补 + [B]_补
    2. [A-B]_补 = [A]_补 + [-B]_补
    3. 符号位与数值位一同参与运算。
    4. 运算结果如果超出最高位(即符号位)产生进位,则该进位直接丢弃image.png

3. 溢出

  • 定义: 计算结果超出了机器所能表示的数的范围。
  • 溢出检测逻辑: 仅当两个符号相同的数相加时才可能发生溢出。可以通过检测最高有效位(符号位)的进位 C_n 和次高有效位的进位 C_(n-1) 来判断。
  • 溢出判断公式: V = C_n ⊕ C_(n-1)
    • V=1 时,表示发生溢出。
    • V=0 时,表示未发生溢出。

4. 补码加减法器

通过使用异或门(XOR)来控制加数 B 是否取反,并利用控制信号同时作为最低位的进位输入,可以用一套电路同时实现加法和减法。

  • 当控制信号 S=0 时,执行 A+B
  • 当控制信号 S=1 时,执行 A-B (即 A + B的反码 + 1)。
    image.png

Verilog HDL

核心思维转变:从软件到硬件

在学习 Verilog 之前,必须建立一个认知:你不是在写代码,你是在画电路。

  • 软件(C/Python): 一行行顺序执行(CPU 读一行跑一行)。
  • Verilog: 并发执行(Parallel)。所有的 assign 语句、所有的模块实例、所有的 always 块,在电路通电的那一刻,是同时工作的。

模块(Module):电路的“黑盒子”

Verilog 的基本单位是“模块”。你可以把它想象成一块芯片,或者一个封装好的电路板。

基础骨架

module 模块名 (
    // 1. 端口列表:对外的引脚
    input        clk,    // 输入信号
    input  [7:0] a,      // 8位宽的输入信号
    output [7:0] sum     // 8位宽的输出信号
);

    // 2. 内部变量声明:盒子里的连线和存储
    wire [7:0] temp;
    reg  [7:0] result;

    // 3. 功能描述:电路逻辑
    // ... (逻辑代码)

endmodule

关键要素

  • 标识符:区分大小写(aA 不一样)。通常以字母或下划线开头。
  • 四值逻辑
    • 0:低电平
    • 1:高电平
    • X:未知/不定态(通常因未初始化或逻辑冲突产生,调试时的噩梦)。
    • Z:高阻态(断路状态,常用于三态门总线)。

数据类型:Wire vs Reg (重难点)

这是新手最容易混淆的地方:

类型关键字物理含义赋值场景
线网型wire物理连线。它不存数据,只负责导电。必须配合 assign 语句使用。
寄存器型reg数据存储(类似变量)。保持当前值直到下次赋值。必须在 过程块 (always, initial) 内部被赋值。

💡 记忆口诀:

  • 用了 assign 必须定义为 wire
  • 用了 alwaysinitial 左边被赋值的必须定义为 reg

描述电路的三种方式

PPT 中用 4选1 多路选择器 (MUX) 展示了 Verilog 描述电路的三种层次:

A. 结构化描述 (Structural) - "像搭积木"

  • 方法:直接调用 Verilog 内置的 12种基本门级原语 (Primitives)
  • 内置原语列表
    • 多输入门andnandornorxor, xnor
      • 用法:and (out, in1, in2, ...) (第一个是输出,后面全是输入)
    • 多输出门buf (缓冲器), not (非门)
    • 三态门bufif1bufif0notif1notif0 (带控制端的门)
  • 特点:代码最长,最接近底层电路图,平时写设计很少用,但看网表(Netlist)时会用到。
    image.png

B. 数据流描述 (Dataflow) - "写公式"

  • 核心关键字: assign (持续赋值语句)。
  • 特点: 描述信号的流动,类似布尔代数公式。只要右边变化,左边立刻更新(并发)。
  • 常用场景: 组合逻辑。
  • 例子:
    // 逻辑表达式法
    assign Y = (~S[1] & ~S[0] & I[0]) | ... ;
    
    // 条件运算法 (更常用)
    assign Y = (S == 2'b00) ? I[0] :
               (S == 2'b01) ? I[1] : ... ;

C. 行为描述 (Behavioral) - "写逻辑"

  • 核心关键字: always
  • 特点: 像写软件一样用 if-else, case 描述功能,编译器会自动帮你生成对应的电路。
  • 例子:
    always @(S or I) begin
        case (S)
            2'b00: Y = I[0];
            2'b01: Y = I[1];
            // 必须写 default 防止锁存器
            default: Y = 1'bx; 
        endcase
    end

过程语句:initial 与 always

initial

  • 执行次数: 只执行一次。
  • 用途: 几乎只用于仿真测试 (Testbench),用来给信号赋初值。不能用于可综合的硬件电路设计(因为电路通电后无法记住"只做一次")。

always (核心)

  • 执行方式: 只要满足触发条件,就不断重复执行。
  • 敏感列表 @(...)
    • 组合逻辑: always @(*)always @(a or b)。只要输入变了,里面就跑一遍。
    • 时序逻辑: always @(posedge clk)。只有时钟上升沿那一瞬间执行(打节拍)。
      image.png

赋值的深坑:阻塞 vs 非阻塞

这是必考题,也是电路跑飞的常见原因。

阻塞赋值 (=)

  • 符号: =
  • 行为: 顺序执行。上一句执行完,才执行下一句。
  • 场景: 用于 组合逻辑always @(*))。

非阻塞赋值 (<=)

  • 符号: <=
  • 行为: 并行执行。块内所有语句的右值先计算好,在时钟沿到来时同时更新给左值。
  • 场景: 用于 时序逻辑always @(posedge clk))。

⚠️ 黄金法则 (Golden Rule):

  1. 描述时序电路(带时钟 clk 的),一律用 <=
  2. 描述组合电路(不带 clk 的),一律用 =
  3. 绝对不要在同一个 always 块里混用这两种赋值。

重要语法细节

条件语句 (if/case)

  • if-else / case:只能在 initial 或 always 内部使用。
  • 严重警告:描述组合逻辑时,如果 if 没有 else,或者 case 没有 default,综合器会生成锁存器 (Latch)。这是设计大忌!
    • 原理:如果你没告诉电路“其他情况”输出什么,电路只能“保持上一次的值”,这就变成了锁存器。

模块实例化 (Instantiation)

这是实现层次化设计(自顶向下/自底向上)的关键。

  • 核心概念:像在电路板上插芯片一样,把写好的模块(如 half_adder)放到当前模块里。
  • 两种关联方式
    1. 位置关联module_name u1 (a, b, c);

      • 缺点:必须严格记住端口顺序,容易出错。
    2. 名称关联 (推荐)module_name u1 (.端口名(信号名), .端口名(信号名));

      • 优点:不容易接错线,顺序乱了也没事。
        怎么把小模块连成大系统?
        建议使用名称关联(不易出错),不要用位置关联。
// 推荐:名称关联 (.端口名(信号名))
half_adder u1 (
    .a  (signal_a), 
    .b  (signal_b),
    .sum(signal_sum)
);

// 不推荐:位置关联 (容易接错线)
half_adder u2 (signal_a, signal_b, signal_sum);

常用运算符与数字表示

image.png
PPT 给出了详细的表格,这里按优先级从高到低整理(越靠上优先级越高):

类别符号说明例子
取反/非!, ~!逻辑非, ~按位取反!a (真变假), ~a (1010变0101)
算术*, /, %乘、除、取模硬件设计尽量少用除法和取模
算术+, -加、减
移位<<, >>左移、右移a << 2 (左移2位)
关系<, <=, >, >=小于、小于等于...返回结果是 1 或 0
相等==, !=等于、不等于a == b
按位与&每一位都进行与操作4'b1100 & 4'b1010 = 4'b1000
按位异或^, ^~异或、同或
按位或``
逻辑与&&逻辑判断(a>b) && (b>c)
逻辑或``
三目(条件)? :常见a ? b : c (a真则b,否则c)
拼接符{a, b},{}将信号拼接{cout, sum} = a + b 利用拼接位接收进位

实战项目:使用 Verilog 设计带暂停和复位功能的数字秒表(0-9 计数器)

这是一个非常经典的入门级实战项目:带暂停和复位功能的数字秒表(0-9 计数器)

这个例子非常完美,因为它涵盖了 Verilog 学习中最核心的知识点:

  1. 时序逻辑(计数器):用到 always @(posedge clk) 和非阻塞赋值 <=
  2. 组合逻辑(数码管解码):用到 always @(*)case 语句和阻塞赋值 =
  3. 层次化设计(模块实例化):如何把两个小模块连起来。
  4. 控制信号:如何处理复位(Reset)和使能(Enable)。

这里的电路结构图

我们将设计一个顶层模块 Top_Stopwatch,它内部包含两个子模块:

  1. Counter_10:负责核心计数逻辑(0, 1, ... 9, 0 ...)。
  2. Decoder_7Seg:负责把数字翻译成数码管能亮的信号。
       [顶层模块 Top_Stopwatch]
       |
输入 --+--> [ 子模块1: 计数器 ] --(wire data)---> [ 子模块2: 译码器 ] --+--> 输出
(clk,  |    (Counter_10)                        (Decoder_7Seg)     |    (seg_led)
rst,   |                                                           |
start) +-----------------------------------------------------------+

Verilog 代码分析

代码核心解析
1. 为什么有的地方用 reg,有的用 wire

这是在 Top 模块中最容易晕的地方。

  • 在 Top 模块里current_num 是一根物理连线,连接两个子模块,所以它是 wire
  • 在 Counter 模块内部cnt_out 需要在 always 块里被赋值,并且需要在时钟跳变前记住上一次的值(比如上一次是3,这次才能变成4),所以它必须是 reg
2. 为什么计数器用 <=,译码器用 =
  • 计数器 (Sequential):它需要“打拍子”。在时钟上升沿到来的那一瞬间,我们希望所有的寄存器同时更新状态,不分先后,所以用非阻塞赋值 <=
  • 译码器 (Combinational):它是一个“翻译机”。只要输入变了(比如3变成了4),输出马上就要跟着变,不需要等时钟,逻辑上有先后依赖关系,所以用阻塞赋值 =
3. 这里的 rst_n 是什么意思?
  • rst 是 Reset 的缩写。
  • _n_b 后缀通常表示 Active Low(低电平有效)
  • if (!rst_n):意思是如果 rst_n 是 0(按下了复位键),条件成立,执行复位操作。这是硬件设计的行业规范。
4. 实例化时的 .a(b) 格式
Counter_10 u_counter (
    .clk     (sys_clk), 
    ...
);
  • .clk子模块里定义的名字(插座名)。
  • (sys_clk)顶层模块里的信号名(插头名)。
  • 这种写法叫名称关联,即使你把顺序写乱了,编译器也能正确连上线。千万不要用位置关联(只写括号里的),那个容易出错。
实现代码
1. 子模块一:核心计数器 (Counter_10. v)
点击查看代码
// 模块功能:带异步复位和暂停功能的十进制计数器
module Counter_10 (
    input            clk,      // 时钟信号
    input            rst_n,    // 复位信号 (n表示低电平有效,即0为复位)
    input            en,       // 使能信号 (1=开始计数,0=暂停)
    output reg [3:0] cnt_out   // 4位输出 (0~9只需要4位,最大是1001)
);

    // ======= 时序逻辑电路 =======
    // 黄金法则:时序逻辑用非阻塞赋值 (<=)
    // 敏感列表:时钟上升沿 OR 复位下降沿 触发
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            // 1. 异步复位:最高优先级
            // 当 rst_n 为 0 时,不管时钟怎样,立马清零
            cnt_out <= 4'd0; 
        end
        else if (en) begin
            // 2. 正常计数逻辑:只有 en 为 1 时才工作
            if (cnt_out == 4'd9)
                cnt_out <= 4'd0; // 计满到9,归零
            else
                cnt_out <= cnt_out + 1'b1; // 没满,加1
        end
        // else (en==0) 的情况省略了
        // 在时序逻辑中,如果不写 else,默认保持原值(锁存上一刻的状态),这正是暂停功能想要的
    end

endmodule
2. 子模块二:数码管译码器 (Decoder_7Seg. v)
点击查看代码
// 模块功能:将4位二进制数转换为7段数码管的控制信号
// 假设是共阴极数码管:1亮,0灭
module Decoder_7Seg (
    input  [3:0] data_in,  // 输入的数字 (0-9)
    output reg [6:0] seg_out // 输出的7段段码 (a,b,c,d,e,f,g)
);

    // ======= 组合逻辑电路 =======
    // 黄金法则:组合逻辑用阻塞赋值 (=)
    // 敏感列表:* 代表只要输入(data_in)变化,就执行
    always @(*) begin
        case (data_in)
            // 根据真值表翻译
            // 格式:数字 : 输出 = 7位二进制段码 (对应 gfe_dcba)
            4'd0 : seg_out = 7'b011_1111; // 显示 0
            4'd1 : seg_out = 7'b000_0110; // 显示 1
            4'd2 : seg_out = 7'b101_1011; // 显示 2
            4'd3 : seg_out = 7'b100_1111; // 显示 3
            4'd4 : seg_out = 7'b110_0110; // 显示 4
            4'd5 : seg_out = 7'b110_1101; // 显示 5
            4'd6 : seg_out = 7'b111_1101; // 显示 6
            4'd7 : seg_out = 7'b000_0111; // 显示 7
            4'd8 : seg_out = 7'b111_1111; // 显示 8
            4'd9 : seg_out = 7'b110_1111; // 显示 9
            // 缺省处理:防止生成锁存器(Latch)
            default: seg_out = 7'b000_0000; // 其他情况全灭
        endcase
    end

endmodule
3. 顶层模块:组装电路 (Top_Stopwatch. v)
点击查看代码
// 模块功能:顶层封装,连接计数器和译码器
module Top_Stopwatch (
    input  sys_clk,      // 系统时钟 (比如 FPGA 板上的 50MHz)
    input  sys_rst_n,    // 系统复位按键
    input  sw_start,     // 开始/暂停开关
    output [6:0] led_pins // 连接到物理数码管引脚
);

    // ==== 1. 定义内部连线 (Wire) ====
    // 这根线用来把 计数器的输出 和 译码器的输入 连起来
    // 注意:因为只是连线,不存数据,所以必须用 wire
    wire [3:0] current_num; 

    // ==== 2. 实例化计数器 (就像把芯片插在板子上) ====
    Counter_10 u_counter (
        .clk     (sys_clk),      // 端口连接:模块端口(外部信号)
        .rst_n   (sys_rst_n),
        .en      (sw_start),
        .cnt_out (current_num)   // 输出连到内部网线 current_num 上
    );

    // ==== 3. 实例化译码器 ====
    Decoder_7Seg u_decoder (
        .data_in (current_num),  // 输入从内部网线 current_num 取值
        .seg_out (led_pins)      // 结果直接送到芯片外部引脚
    );

endmodule



0 / 2000
正在加载评论...